feat: add Telegram trading bot for Reya exchange#45
feat: add Telegram trading bot for Reya exchange#450xsmolrun wants to merge 1 commit intoReya-Labs:mainfrom
Conversation
Implements a fully-featured Telegram bot that wraps the Reya Python SDK, allowing users to trade perpetuals and spot markets directly from Telegram. Bot commands: - /prices, /price <SYMBOL>, /markets, /symbols — market data - /accounts, /balance, /positions, /orders, /history — account info - /buy, /sell — IOC limit orders - /buygtc, /sellgtc — GTC limit orders - /sl, /tp — stop-loss and take-profit trigger orders - /cancel <ORDER_ID> — cancel an order Files added: telegram_bot/__init__.py — package marker telegram_bot/main.py — entry point, env loading, access control telegram_bot/bot.py — Telegram Application + all command handlers telegram_bot/trading.py — async TradingService wrapping ReyaTradingClient telegram_bot/formatters.py — Markdown message formatters Also: pyproject.toml — added python-telegram-bot>=21.0 dependency and package .env.example — added TELEGRAM_BOT_TOKEN and ALLOWED_USER_IDS vars Run with: python -m telegram_bot.main https://claude.ai/code/session_01YGfEGdKJQXu8fQHati5XWX
📝 WalkthroughWalkthroughThe changes introduce a Telegram bot module for Reya trading. It includes environment configuration, a TradingService wrapper for the trading SDK, command handlers for market data and order management, message formatters for Telegram output, and a main entrypoint with optional user access control. Changes
Sequence DiagramsequenceDiagram
actor User as Telegram User
participant Bot as Telegram Bot
participant Handler as Command Handler
participant Service as TradingService
participant Client as ReyaTradingClient
participant Formatter as Message Formatter
User->>Bot: /prices command
activate Bot
Bot->>Handler: route command
activate Handler
Handler->>Service: get_prices()
activate Service
Service->>Client: fetch prices
activate Client
Client-->>Service: prices list
deactivate Client
deactivate Service
Service-->>Handler: prices list
Handler->>Formatter: fmt_prices(prices)
activate Formatter
Formatter-->>Handler: markdown string
deactivate Formatter
Handler->>Bot: send formatted message
deactivate Handler
Bot-->>User: market prices display
deactivate Bot
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pyproject.toml`:
- Around line 38-39: Remove the "python-telegram-bot>=21.0,<22.0" entry from the
core dependencies list and add it as an optional extra so only consumers who opt
into the bot feature install it; specifically, locate the dependency string
"python-telegram-bot>=21.0,<22.0" in pyproject.toml and delete it from the main
dependencies, then add it under the PEP-621 optional dependencies section (e.g.,
add an entry under [project.optional-dependencies] with a key like "telegram"
whose value is ["python-telegram-bot>=21.0,<22.0"]); ensure the extras key name
("telegram") matches any docs or usage in the repo that reference installing the
telegram feature.
In `@telegram_bot/bot.py`:
- Around line 211-220: The _parse_order_args function currently only attempts
float(qty)/float(price) which allows zero, negatives, NaN, and infinite values;
update _parse_order_args to convert qty and price to floats, then reject values
that are <= 0 or not finite (use math.isfinite), raising ValueError with the
same usage/help text if validation fails; keep returning symbol, qty, price
(either as validated floats or original strings per the surrounding API
expectations) and apply the same finite/positive checks to the other
order-parsing/validation block mentioned in the review.
- Around line 39-45: The help text mislabels IOC orders as “fill-or-kill” (FOK);
update the trading help block that lists the /buy, /sell, /buygtc and /sellgtc
command descriptions to correctly describe IOC as "Immediate-or-Cancel (IOC) —
executes immediately and cancels any unfilled portion (partial fills allowed)"
and keep GTC described as resting limit orders; locate the help text string in
bot.py (the block that contains "*Trading — Perp / Spot IOC (fill-or-kill)*" and
replace the wording accordingly so the /buy and /sell entries reference IOC
semantics rather than FOK.
- Around line 10-12: The _reply helper (and the other try/except at lines
~64-69) currently catches all exceptions; change them to catch only
telegram.error.BadRequest and then inspect the exception message for
parse-related indicators (e.g., "can't parse entities", "Can't parse", or
similar parse/markdown/html errors) before falling back to plain text; for any
other BadRequest messages or any other exception types re-raise the exception so
network/Telegram API errors are not swallowed. Ensure you import BadRequest from
telegram.error and update both exception blocks (the _reply function and the
second handler around lines 64-69) to follow this narrow-catch-and-inspect
pattern.
In `@telegram_bot/formatters.py`:
- Around line 147-153: The loop in summaries uses non-existent attributes
last_price and volume_24h on MarketSummary; replace them with the actual model
fields throttled_oracle_price and volume24h (e.g., get throttled_oracle_price
into last_raw and pass it to _oracle_price_usd, and get volume24h into
volume_raw and format as before), using getattr(s, "throttled_oracle_price",
None) and getattr(s, "volume24h", None) so the `/markets` output shows real
values.
- Around line 89-103: The loop that formats orders uses getattr(o, "is_buy",
True) causing SDK Order objects (which expose side, not is_buy) to be
mis-labeled; update the logic in the orders formatting block to prefer
getattr(o, "side", None) and map its string (e.g., "buy"/"Buy" or "sell"/"Sell")
to the display "Buy"/"Sell", falling back to using getattr(o, "is_buy", None)
(mapping True→"Buy", False→"Sell") only if side is missing, and use a safe
default like "N/A" if neither is present; adjust the reference in the
lines.append construction so the variable used for display is the normalized
side string instead of the current side expression.
- Around line 68-80: fmt_positions is reading non-existent SDK fields (is_long,
size, unrealized_pnl); update it to use the SDK model's actual attributes: use
pos.side (map to "Long"/"Short" as appropriate) instead of is_long, use pos.qty
for size display (format pos.qty / 1e18) instead of size, and use
pos.avg_entry_price for entry price display; remove or fallback to "N/A" for
unrealized_pnl (or use any available pnl field from the SDK if present) so the
formatter shows real data from the SDK model rather than default values.
In `@telegram_bot/main.py`:
- Around line 78-81: The code currently logs a warning when ALLOWED_USER_IDS is
unset, which leaves the bot open; change the behavior to fail-closed by
requiring ALLOWED_USER_IDS unless an explicit opt-in flag (e.g.,
TELEGRAM_PUBLIC_MODE=true) is set. Update the check around allowed_user_ids in
main.py (the variable allowed_user_ids and the logger calls) to: if
allowed_user_ids is empty and TELEGRAM_PUBLIC_MODE is not truthy, log an error
and exit (or raise SystemExit); if TELEGRAM_PUBLIC_MODE is set, keep the
existing info/warning behavior. Apply the same change to the other occurrence
referenced (lines 95-96) so both checks enforce the fail-closed policy.
- Around line 126-139: The _check_user handler currently raises a generic
Exception to stop processing unauthorized updates; change this to raise
telegram.ext.ApplicationHandlerStop (import ApplicationHandlerStop) so PTB
treats it as normal short-circuit control flow rather than an error, and keep
the existing reply_text call before raising ApplicationHandlerStop in
_check_user (or simply return after sending the reply if you prefer to avoid
raising); update the import list to include ApplicationHandlerStop and remove
the generic Exception usage in the TypeHandler(Update, _check_user) flow.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 04ac8674-4eac-4e1a-aa7c-3bc2ad28729b
📒 Files selected for processing (7)
.env.examplepyproject.tomltelegram_bot/__init__.pytelegram_bot/bot.pytelegram_bot/formatters.pytelegram_bot/main.pytelegram_bot/trading.py
| "aiohttp-retry>=2.8.3,<3.0.0", | ||
| "python-telegram-bot>=21.0,<22.0" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Read pyproject.toml to check the dependencies and optional-dependencies sections
cat -n pyproject.toml | head -100Repository: Reya-Labs/reya-python-sdk
Length of output: 3603
🏁 Script executed:
# Find the telegram_bot module mentioned in the comment
find . -type f -name "*telegram*" | head -20Repository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Check git diff or recent changes to see what was actually added
git diff HEAD~1 pyproject.toml 2>/dev/null || echo "Git diff not available, showing current state"Repository: Reya-Labs/reya-python-sdk
Length of output: 114
🏁 Script executed:
# Check if telegram_bot directory exists and what it contains
find . -type d -name "telegram_bot" -o -type f -name "telegram_bot*"Repository: Reya-Labs/reya-python-sdk
Length of output: 83
🏁 Script executed:
# If telegram_bot exists, examine its structure
ls -la telegram_bot/ 2>/dev/null && find telegram_bot -type f | head -20Repository: Reya-Labs/reya-python-sdk
Length of output: 608
🏁 Script executed:
# Search for any imports of telegram_bot in the main SDK
rg "from telegram_bot|import telegram_bot" --type pyRepository: Reya-Labs/reya-python-sdk
Length of output: 329
🏁 Script executed:
# Also check if python-telegram-bot is imported anywhere in core SDK
rg "from telegram_bot|import telegram" --type py | grep -v "telegram_bot/" | head -20Repository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Check the main SDK structure and what's exposed in __init__.py
find . -type f -name "__init__.py" | grep -E "^\./(sdk/)?__init__\.py$" | xargs cat -nRepository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Search for any use of python-telegram-bot package in the SDK core
rg "python.telegram.bot|from telegram|import telegram" sdk/ --type py 2>/dev/null | head -20Repository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Check if telegram_bot is imported or referenced in the main package __init__
cat -n sdk/__init__.py 2>/dev/null || cat -n __init__.py 2>/dev/null || echo "No main __init__ found"Repository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# List all Python files in sdk/ to understand core SDK structure
find sdk -name "*.py" -type f | head -30Repository: Reya-Labs/reya-python-sdk
Length of output: 1123
🏁 Script executed:
# Read sdk/__init__.py to see what's exported
cat -n sdk/__init__.pyRepository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Search for any imports from telegram in the entire SDK (not telegram_bot package)
rg "from telegram|import telegram" . --type py -g "!telegram_bot/" 2>/dev/nullRepository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Check if telegram_bot is referenced in any SDK code, examples, or documentation
rg "telegram_bot" sdk/ examples/ --type py 2>/dev/null | head -20Repository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Check examples directory to understand how telegram_bot is meant to be used
find examples -type f -name "*.py" 2>/dev/null | xargs ls -laRepository: Reya-Labs/reya-python-sdk
Length of output: 2660
🏁 Script executed:
# Look at the telegram_bot/__init__.py to see what's exported
cat -n telegram_bot/__init__.pyRepository: Reya-Labs/reya-python-sdk
Length of output: 121
🏁 Script executed:
# Check the README to see if telegram bot is documented as a core feature or optional feature
head -50 README.mdRepository: Reya-Labs/reya-python-sdk
Length of output: 2224
Move python-telegram-bot to an optional extra.
The python-telegram-bot>=21.0,<22.0 dependency is currently in the core SDK dependencies but is only used by the self-contained telegram_bot module. The core SDK and its examples have zero dependency on this library. Including it in base installations forces every consumer to download and maintain an unnecessary dependency, even those who never use the bot. Move python-telegram-bot behind an optional extra (e.g., telegram) so only users who explicitly use the bot feature install it.
Proposed packaging change
dependencies = [
"web3>=7.12.1,<8.0.0",
"eth-account>=0.13.7,<1.0.0",
"eth-abi>=5.2.0,<6.0.0",
"typing-extensions>=4.14.1,<5.0.0",
"python-dotenv>=1.1.1,<2.0.0",
"requests>=2.32.4,<3.0.0",
"httpx>=0.28.1,<1.0.0",
"websocket-client>=1.8.0,<2.0.0",
"tomli>=2.0.1,<3.0.0",
"types-requests>=2.31.0,<2.32.0",
"python-dateutil>=2.8.0,<3.0.0",
"aiohttp-retry>=2.8.3,<3.0.0",
- "python-telegram-bot>=21.0,<22.0"
]
[project.optional-dependencies]
dev = [
...
]
+telegram = [
+ "python-telegram-bot>=21.0,<22.0",
+]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "aiohttp-retry>=2.8.3,<3.0.0", | |
| "python-telegram-bot>=21.0,<22.0" | |
| "aiohttp-retry>=2.8.3,<3.0.0", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pyproject.toml` around lines 38 - 39, Remove the
"python-telegram-bot>=21.0,<22.0" entry from the core dependencies list and add
it as an optional extra so only consumers who opt into the bot feature install
it; specifically, locate the dependency string "python-telegram-bot>=21.0,<22.0"
in pyproject.toml and delete it from the main dependencies, then add it under
the PEP-621 optional dependencies section (e.g., add an entry under
[project.optional-dependencies] with a key like "telegram" whose value is
["python-telegram-bot>=21.0,<22.0"]); ensure the extras key name ("telegram")
matches any docs or usage in the repo that reference installing the telegram
feature.
| from telegram import Update | ||
| from telegram.constants import ParseMode | ||
| from telegram.ext import Application, CommandHandler, ContextTypes |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | grep -E "telegram_bot|bot\.py"Repository: Reya-Labs/reya-python-sdk
Length of output: 185
🏁 Script executed:
cat -n telegram_bot/bot.pyRepository: Reya-Labs/reya-python-sdk
Length of output: 18797
🌐 Web query:
telegram-bot-api python BadRequest exception parse error
💡 Result:
In Telegram’s Bot API, a Python BadRequest with a parse error almost always means Telegram rejected the formatting or JSON you sent, not that your HTTP request failed.
1) Bad Request: can't parse entities ... (most common)
You sent parse_mode="HTML" or parse_mode="MarkdownV2" (or Markdown) and the text contains invalid / unescaped markup, e.g.:
- HTML: unsupported tags (
<br>), malformed tags, missing closing tags, raw</&that should be escaped. Aiogram shows this exact failure pattern with “Unsupported start tag … at byte offset …”. [2] - MarkdownV2: reserved characters not escaped (e.g.
(,),_,*,[,], etc.), or unbalanced formatting markers—this commonly triggers “Can’t find end of the entity …”. [4]
Fix options (pick one):
- Disable parsing (send plain text): don’t set
parse_mode. - Escape your text:
- python-telegram-bot: use
telegram.utils.helpers.escape_markdown(...)before sending with Markdown/MarkdownV2. [3] - For HTML parse mode: escape user-generated text with
html.escape(...)and only add a small, valid subset of tags yourself (Telegram only supports a limited HTML subset). [1]
- python-telegram-bot: use
- Avoid
parse_modeentirely by sending explicitentities(message entities) instead of markup. (Supported by many libraries and the Bot API conceptually.) [1]
2) Bad Request: can't parse reply keyboard markup JSON object / other “can’t parse … JSON”
Your reply_markup / payload is not valid JSON (common when sending dicts via querystring, wrong nesting, or not JSON-serializing).
Fix:
- Send as POST JSON (not URL params) and ensure
reply_markupis properly serialized (your library usually does this if you pass the right Python objects).
3) Use the offset to find the culprit
The error’s “byte offset …” points to where Telegram’s parser got confused—inspect that character position in the final string you sent (after any templating/formatting).
If you paste the exact exception text + the message text (and which library: python-telegram-bot, aiogram, pyTelegramBotAPI, or raw requests), I can point to the specific character/tag and show the minimal escaping needed.
🌐 Web query:
python-telegram-bot BadRequest exception telegram.error
💡 Result:
In python-telegram-bot (PTB), telegram.error.BadRequest is the library’s wrapper for Telegram Bot API “400 Bad Request” responses—i.e., Telegram accepted the HTTP request but rejected the parameters/state as invalid. (docs.python-telegram-bot.org)
What to do first
- Print/log the exact error text (it’s in the exception message). PTB’s
BadRequestmessage usually contains the Telegram reason like:Message is not modifiedChat not foundMessage to edit not found
- Handle it either locally (try/except) or via PTB’s global error handler. (github-wiki-see.page)
Common BadRequest causes (and fixes)
1) Message is not modified
Happens when you call edit_message_text / edit_message_reply_markup but the new text/markup is identical to the current one. Fix: compare before editing, or ignore that specific error. (stackoverflow.com)
2) Chat not found
Usually means the chat_id is wrong, you’re trying to message a user who hasn’t started the bot, the bot isn’t in the group/channel, or you’re using the wrong ID type (e.g., missing -100... for many channels/supergroups). (github.com)
3) Message to edit not found
You’re trying to edit a message that doesn’t exist (wrong message_id), is too old/invalid to edit, or belongs to a different chat. (stackoverflow.com)
Minimal handling pattern
from telegram.error import BadRequest, TelegramError
try:
await update.callback_query.edit_message_text("new text")
except BadRequest as e:
if "Message is not modified" in str(e):
return # safe to ignore
raise
except TelegramError:
raise(Using a global error handler is also supported/recommended for centralized logging/handling.) (github-wiki-see.page)
If you paste the exact exception text (full line after BadRequest:) and the API call you’re making (e.g., send_message, edit_message_text, etc.), I can map it to the specific fix.
Citations:
- 1: https://docs.python-telegram-bot.org/en/latest/telegram.error.html?utm_source=openai
- 2: https://github-wiki-see.page/m/python-telegram-bot/python-telegram-bot/wiki/Exceptions%2C-Warnings-and-Logging?utm_source=openai
- 3: https://stackoverflow.com/questions/60862027/telegram-bot-with-python-telegram-error-badrequest-message-is-not-modified?utm_source=openai
- 4: telegram.error.BadRequest: Chat not found python-telegram-bot/python-telegram-bot#3047
- 5: https://stackoverflow.com/questions/68152188/telegram-error-badrequest-message-to-edit-not-found?utm_source=openai
- 6: https://github-wiki-see.page/m/python-telegram-bot/python-telegram-bot/wiki/Exceptions%2C-Warnings-and-Logging?utm_source=openai
🌐 Web query:
python-telegram-bot reply_text exceptions TelegramError BadRequest NetworkError
💡 Result:
In python-telegram-bot (PTB), await message.reply_text(...) ultimately makes a Bot API request (i.e., “sendMessage”), and failures are raised as telegram.error.TelegramError (base class) or one of its subclasses. PTB’s exception types live in telegram.error. [1]
The 3 you mentioned (what they mean)
TelegramError: Catch-all base class for PTB/Telegram-related errors. [1]BadRequest(subclass ofNetworkError): Telegram Bot API returned HTTP 400 because your request parameters/content are invalid (e.g., empty/too-long text, invalidparse_mode/entities, bad markup, wrong IDs, “message thread not found”, etc.). [1][2]NetworkError: Network/communication layer problems when talking to Telegram (connectivity, protocol issues, transient failures, etc.).BadRequestis also under this umbrella in PTB’s hierarchy. [1]
Typical handling pattern
from telegram.error import BadRequest, NetworkError, TelegramError
try:
await update.message.reply_text(text, parse_mode="HTML")
except BadRequest as e:
# Fix the request: bad HTML/entities, empty text, invalid reply_markup, wrong thread/chat, etc.
logger.warning("BadRequest: %s", e)
except NetworkError as e:
# Transient: you may retry/backoff
logger.warning("NetworkError: %s", e)
except TelegramError as e:
# Anything else PTB raises
logger.exception("TelegramError: %s", e)If you share the exact exception message (e.g., "Bad Request: can't parse entities", "message text is empty", "message thread not found"), I can map it to the most likely concrete cause and fix.
Sources: PTB telegram.error docs (exception definitions & hierarchy). [1] PTB error-handling notes/examples of common BadRequest scenarios. [2]
[1] (docs.python-telegram-bot.org)
[2] (deepwiki.com)
Citations:
- 1: https://docs.python-telegram-bot.org/en/latest/telegram.error.html?utm_source=openai
- 2: https://deepwiki.com/python-telegram-bot/python-telegram-bot/2.5-error-handling-and-exception-classes?utm_source=openai
Narrow exception handling to parse errors only.
The _reply helper currently catches all exceptions and silently falls back to plain text, which masks unrelated Telegram API errors and network failures. This should catch only BadRequest exceptions with parse-related messages, allowing other errors to propagate.
Suggested exception narrowing
from telegram import Update
+from telegram.error import BadRequest
from telegram.constants import ParseMode
from telegram.ext import Application, CommandHandler, ContextTypes
...
async def _reply(update: Update, text: str) -> None:
"""Send a Markdown reply, falling back to plain text on parse errors."""
try:
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
- except Exception:
+ except BadRequest as exc:
+ if "parse" not in str(exc).lower():
+ raise
await update.message.reply_text(text)Also applies to: 64-69
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/bot.py` around lines 10 - 12, The _reply helper (and the other
try/except at lines ~64-69) currently catches all exceptions; change them to
catch only telegram.error.BadRequest and then inspect the exception message for
parse-related indicators (e.g., "can't parse entities", "Can't parse", or
similar parse/markdown/html errors) before falling back to plain text; for any
other BadRequest messages or any other exception types re-raise the exception so
network/Telegram API errors are not swallowed. Ensure you import BadRequest from
telegram.error and update both exception blocks (the _reply function and the
second handler around lines 64-69) to follow this narrow-catch-and-inspect
pattern.
| *Trading — Perp / Spot IOC (fill-or-kill)* | ||
| `/buy <SYMBOL> <QTY> <PRICE>` — Limit buy (IOC) | ||
| `/sell <SYMBOL> <QTY> <PRICE>` — Limit sell (IOC) | ||
|
|
||
| *Trading — Perp GTC (resting limit order)* | ||
| `/buygtc <SYMBOL> <QTY> <PRICE>` — Limit buy (GTC) | ||
| `/sellgtc <SYMBOL> <QTY> <PRICE>` — Limit sell (GTC) |
There was a problem hiding this comment.
Document IOC correctly; the current help text describes FOK semantics.
IOC orders can partially fill and cancel the remainder. Calling them “fill-or-kill” tells users the order is all-or-nothing, which is a materially different trading behavior.
Suggested wording fix
-*Trading — Perp / Spot IOC (fill-or-kill)*
+*Trading — Perp / Spot IOC (immediate-or-cancel)*📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| *Trading — Perp / Spot IOC (fill-or-kill)* | |
| `/buy <SYMBOL> <QTY> <PRICE>` — Limit buy (IOC) | |
| `/sell <SYMBOL> <QTY> <PRICE>` — Limit sell (IOC) | |
| *Trading — Perp GTC (resting limit order)* | |
| `/buygtc <SYMBOL> <QTY> <PRICE>` — Limit buy (GTC) | |
| `/sellgtc <SYMBOL> <QTY> <PRICE>` — Limit sell (GTC) | |
| *Trading — Perp / Spot IOC (immediate-or-cancel)* | |
| `/buy <SYMBOL> <QTY> <PRICE>` — Limit buy (IOC) | |
| `/sell <SYMBOL> <QTY> <PRICE>` — Limit sell (IOC) | |
| *Trading — Perp GTC (resting limit order)* | |
| `/buygtc <SYMBOL> <QTY> <PRICE>` — Limit buy (GTC) | |
| `/sellgtc <SYMBOL> <QTY> <PRICE>` — Limit sell (GTC) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/bot.py` around lines 39 - 45, The help text mislabels IOC orders
as “fill-or-kill” (FOK); update the trading help block that lists the /buy,
/sell, /buygtc and /sellgtc command descriptions to correctly describe IOC as
"Immediate-or-Cancel (IOC) — executes immediately and cancels any unfilled
portion (partial fills allowed)" and keep GTC described as resting limit orders;
locate the help text string in bot.py (the block that contains "*Trading — Perp
/ Spot IOC (fill-or-kill)*" and replace the wording accordingly so the /buy and
/sell entries reference IOC semantics rather than FOK.
| def _parse_order_args(args: list[str]) -> tuple[str, str, str]: | ||
| """Parse and validate <SYMBOL> <QTY> <PRICE> args. Returns (symbol, qty, price).""" | ||
| if len(args) < 3: | ||
| raise ValueError("Usage: `<SYMBOL> <QTY> <PRICE>`") | ||
| symbol = args[0].upper() | ||
| qty = args[1] | ||
| price = args[2] | ||
| float(qty) # validate numeric | ||
| float(price) # validate numeric | ||
| return symbol, qty, price |
There was a problem hiding this comment.
Reject non-positive and non-finite numeric inputs before order submission.
float(...) only checks that the string parses. Right now 0, negative values, nan, and inf all pass validation and can reach the trading API for both order placement and trigger creation.
Suggested validation hardening
+import math
...
+def _parse_positive_number(name: str, raw: str) -> str:
+ value = float(raw)
+ if not math.isfinite(value) or value <= 0:
+ raise ValueError(f"{name} must be a positive finite number.")
+ return raw
+
...
def _parse_order_args(args: list[str]) -> tuple[str, str, str]:
"""Parse and validate <SYMBOL> <QTY> <PRICE> args. Returns (symbol, qty, price)."""
if len(args) < 3:
raise ValueError("Usage: `<SYMBOL> <QTY> <PRICE>`")
symbol = args[0].upper()
- qty = args[1]
- price = args[2]
- float(qty) # validate numeric
- float(price) # validate numeric
+ qty = _parse_positive_number("QTY", args[1])
+ price = _parse_positive_number("PRICE", args[2])
return symbol, qty, price
...
def _parse_trigger_args(args: list[str]) -> tuple[str, bool, str]:
"""Parse <SYMBOL> <buy|sell> <TRIGGER_PRICE>. Returns (symbol, is_buy, trigger_px)."""
if len(args) < 3:
raise ValueError("Usage: `<SYMBOL> <buy|sell> <TRIGGER_PRICE>`")
symbol = args[0].upper()
side = args[1].lower()
if side not in ("buy", "sell"):
raise ValueError("Side must be `buy` or `sell`.")
is_buy = side == "buy"
- trigger_px = args[2]
- float(trigger_px) # validate numeric
+ trigger_px = _parse_positive_number("TRIGGER_PRICE", args[2])
return symbol, is_buy, trigger_pxAlso applies to: 301-312
🧰 Tools
🪛 Ruff (0.15.4)
[warning] 214-214: Avoid specifying long messages outside the exception class
(TRY003)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/bot.py` around lines 211 - 220, The _parse_order_args function
currently only attempts float(qty)/float(price) which allows zero, negatives,
NaN, and infinite values; update _parse_order_args to convert qty and price to
floats, then reject values that are <= 0 or not finite (use math.isfinite),
raising ValueError with the same usage/help text if validation fails; keep
returning symbol, qty, price (either as validated floats or original strings per
the surrounding API expectations) and apply the same finite/positive checks to
the other order-parsing/validation block mentioned in the review.
| for pos in positions: | ||
| symbol = getattr(pos, "symbol", "N/A") | ||
| side = "Long" if getattr(pos, "is_long", True) else "Short" | ||
| size_raw = getattr(pos, "size", None) | ||
| size_str = f"{float(size_raw) / 1e18:,.6f}" if size_raw else "N/A" | ||
| entry_raw = getattr(pos, "avg_entry_price", None) | ||
| entry_str = _oracle_price_usd(entry_raw) if entry_raw else "N/A" | ||
| pnl_raw = getattr(pos, "unrealized_pnl", None) | ||
| pnl_str = f"{float(pnl_raw) / 1e18:,.4f}" if pnl_raw else "N/A" | ||
| lines.append( | ||
| f"`{symbol}` — {side} {size_str}\n" | ||
| f" Entry: {entry_str} | uPnL: {pnl_str} rUSD" | ||
| ) |
There was a problem hiding this comment.
fmt_positions is reading fields the SDK model does not expose.
sdk/open_api/models/position.py:27-41 has side, qty, and avg_entry_price; this formatter looks for is_long, size, and unrealized_pnl. In practice that will default real positions to Long, print N/A for size, and never show actual PnL.
Suggested field alignment
for pos in positions:
symbol = getattr(pos, "symbol", "N/A")
- side = "Long" if getattr(pos, "is_long", True) else "Short"
- size_raw = getattr(pos, "size", None)
+ side = getattr(pos, "side", "N/A")
+ size_raw = getattr(pos, "qty", None)
size_str = f"{float(size_raw) / 1e18:,.6f}" if size_raw else "N/A"
entry_raw = getattr(pos, "avg_entry_price", None)
entry_str = _oracle_price_usd(entry_raw) if entry_raw else "N/A"
- pnl_raw = getattr(pos, "unrealized_pnl", None)
- pnl_str = f"{float(pnl_raw) / 1e18:,.4f}" if pnl_raw else "N/A"
lines.append(
f"`{symbol}` — {side} {size_str}\n"
- f" Entry: {entry_str} | uPnL: {pnl_str} rUSD"
+ f" Entry: {entry_str}"
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/formatters.py` around lines 68 - 80, fmt_positions is reading
non-existent SDK fields (is_long, size, unrealized_pnl); update it to use the
SDK model's actual attributes: use pos.side (map to "Long"/"Short" as
appropriate) instead of is_long, use pos.qty for size display (format pos.qty /
1e18) instead of size, and use pos.avg_entry_price for entry price display;
remove or fallback to "N/A" for unrealized_pnl (or use any available pnl field
from the SDK if present) so the formatter shows real data from the SDK model
rather than default values.
| for o in orders: | ||
| order_id = getattr(o, "order_id", "N/A") | ||
| symbol = getattr(o, "symbol", "N/A") | ||
| side = "Buy" if getattr(o, "is_buy", True) else "Sell" | ||
| qty_raw = getattr(o, "qty", None) | ||
| qty_str = f"{float(qty_raw) / 1e18:,.6f}" if qty_raw else "N/A" | ||
| px_raw = getattr(o, "limit_px", None) | ||
| px_str = _oracle_price_usd(px_raw) if px_raw else "N/A" | ||
| order_type = getattr(o, "order_type", "") | ||
| tif = getattr(o, "time_in_force", "") | ||
| status = getattr(o, "status", "") | ||
| lines.append( | ||
| f"ID `{order_id}` — {side} {qty_str} `{symbol}` @ {px_str}\n" | ||
| f" Type: {order_type} {tif} | Status: {status}" | ||
| ) |
There was a problem hiding this comment.
Open-order side will display incorrectly for SDK orders.
sdk/open_api/models/order.py:30-50 exposes side, not is_buy. With the current getattr(o, "is_buy", True) fallback, sell orders from the SDK render as buys.
Suggested field fix
- side = "Buy" if getattr(o, "is_buy", True) else "Sell"
+ side = getattr(o, "side", "N/A")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/formatters.py` around lines 89 - 103, The loop that formats
orders uses getattr(o, "is_buy", True) causing SDK Order objects (which expose
side, not is_buy) to be mis-labeled; update the logic in the orders formatting
block to prefer getattr(o, "side", None) and map its string (e.g., "buy"/"Buy"
or "sell"/"Sell") to the display "Buy"/"Sell", falling back to using getattr(o,
"is_buy", None) (mapping True→"Buy", False→"Sell") only if side is missing, and
use a safe default like "N/A" if neither is present; adjust the reference in the
lines.append construction so the variable used for display is the normalized
side string instead of the current side expression.
| for s in summaries: | ||
| symbol = getattr(s, "symbol", "N/A") | ||
| last_raw = getattr(s, "last_price", None) | ||
| last_str = _oracle_price_usd(last_raw) if last_raw else "N/A" | ||
| volume_raw = getattr(s, "volume_24h", None) | ||
| volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A" | ||
| lines.append(f"`{symbol}` — Last: {last_str} Vol 24h: {volume_str}") |
There was a problem hiding this comment.
/markets will show N/A because these attributes do not exist on MarketSummary.
sdk/open_api/models/market_summary.py:26-44 exposes throttled_oracle_price and volume24h; last_price and volume_24h are not part of the model.
Suggested field fix
- last_raw = getattr(s, "last_price", None)
+ last_raw = getattr(s, "throttled_oracle_price", None)
last_str = _oracle_price_usd(last_raw) if last_raw else "N/A"
- volume_raw = getattr(s, "volume_24h", None)
+ volume_raw = getattr(s, "volume24h", None)
volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for s in summaries: | |
| symbol = getattr(s, "symbol", "N/A") | |
| last_raw = getattr(s, "last_price", None) | |
| last_str = _oracle_price_usd(last_raw) if last_raw else "N/A" | |
| volume_raw = getattr(s, "volume_24h", None) | |
| volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A" | |
| lines.append(f"`{symbol}` — Last: {last_str} Vol 24h: {volume_str}") | |
| for s in summaries: | |
| symbol = getattr(s, "symbol", "N/A") | |
| last_raw = getattr(s, "throttled_oracle_price", None) | |
| last_str = _oracle_price_usd(last_raw) if last_raw else "N/A" | |
| volume_raw = getattr(s, "volume24h", None) | |
| volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A" | |
| lines.append(f"`{symbol}` — Last: {last_str} Vol 24h: {volume_str}") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/formatters.py` around lines 147 - 153, The loop in summaries
uses non-existent attributes last_price and volume_24h on MarketSummary; replace
them with the actual model fields throttled_oracle_price and volume24h (e.g.,
get throttled_oracle_price into last_raw and pass it to _oracle_price_usd, and
get volume24h into volume_raw and format as before), using getattr(s,
"throttled_oracle_price", None) and getattr(s, "volume24h", None) so the
`/markets` output shows real values.
| if allowed_user_ids: | ||
| logger.info("Access restricted to user IDs: %s", allowed_user_ids) | ||
| else: | ||
| logger.warning("ALLOWED_USER_IDS not set — all Telegram users can interact with the bot") |
There was a problem hiding this comment.
Fail closed when no Telegram allow-list is configured.
With the current flow, omitting ALLOWED_USER_IDS exposes a hot trading bot to any Telegram user who can discover it. For a bot holding signing credentials, that is an authentication bypass and should not be the default. Require an allow-list unless the operator explicitly opts into public mode.
Suggested fail-closed guard
logger.info("Chain ID: %d (%s)", config.chain_id, "mainnet" if config.is_mainnet else "testnet")
- if allowed_user_ids:
+ allow_public_bot = os.environ.get("ALLOW_PUBLIC_BOT", "").lower() == "true"
+ if allowed_user_ids:
logger.info("Access restricted to user IDs: %s", allowed_user_ids)
+ elif allow_public_bot:
+ logger.warning("Running in public mode — all Telegram users can interact with the bot")
else:
- logger.warning("ALLOWED_USER_IDS not set — all Telegram users can interact with the bot")
+ logger.error("ALLOWED_USER_IDS is required unless ALLOW_PUBLIC_BOT=true is set explicitly.")
+ sys.exit(1)
...
- if allowed_user_ids:
+ if allowed_user_ids:
_apply_access_control(app, allowed_user_ids)Also applies to: 95-96
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/main.py` around lines 78 - 81, The code currently logs a warning
when ALLOWED_USER_IDS is unset, which leaves the bot open; change the behavior
to fail-closed by requiring ALLOWED_USER_IDS unless an explicit opt-in flag
(e.g., TELEGRAM_PUBLIC_MODE=true) is set. Update the check around
allowed_user_ids in main.py (the variable allowed_user_ids and the logger calls)
to: if allowed_user_ids is empty and TELEGRAM_PUBLIC_MODE is not truthy, log an
error and exit (or raise SystemExit); if TELEGRAM_PUBLIC_MODE is set, keep the
existing info/warning behavior. Apply the same change to the other occurrence
referenced (lines 95-96) so both checks enforce the fail-closed policy.
| from telegram import Update | ||
| from telegram.ext import TypeHandler | ||
|
|
||
| async def _check_user(update: Update, context) -> None: | ||
| if not update.effective_user: | ||
| return | ||
| uid = update.effective_user.id | ||
| if uid not in allowed_user_ids: | ||
| logger.warning("Rejected update from unauthorised user %d", uid) | ||
| if update.message: | ||
| await update.message.reply_text("You are not authorised to use this bot.") | ||
| raise Exception("Unauthorised") | ||
|
|
||
| app.add_handler(TypeHandler(Update, _check_user), group=-1) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🌐 Web query:
ApplicationHandlerStop python-telegram-bot telegram.ext
💡 Result:
telegram.ext.ApplicationHandlerStop is an exception you can raise inside a handler (or error handler) to stop python-telegram-bot’s Application from running any further handlers for the same update, even across different handler groups. It’s the “hard stop” mechanism for handler dispatching. [1]
Typical use
from telegram.ext import ApplicationHandlerStop
async def my_handler(update, context):
# ... do something ...
raise ApplicationHandlerStop() # stop processing this update in other handlers[1]
With ConversationHandler
Instead of returning the next state, pass it via the optional state argument:
from telegram.ext import ApplicationHandlerStop
async def conversation_callback(update, context):
# ...
raise ApplicationHandlerStop(next_state)[1]
Important caveat
It doesn’t work if the handler/error-handler is executed in a non-blocking way. [1]
(Older v13 docs called the analogous concept DispatcherHandlerStop; in v20+ it’s ApplicationHandlerStop.) [1]
Sources: python-telegram-bot docs for ApplicationHandlerStop. [1]
🏁 Script executed:
# Search for ApplicationHandlerStop in the codebase if it exists
rg "ApplicationHandlerStop" .Repository: Reya-Labs/reya-python-sdk
Length of output: 51
🌐 Web query:
python-telegram-bot exception handling handlers error pipeline
💡 Result:
In python-telegram-bot (PTB) v20+, the unhandled-exception pipeline works like this:
- If a handler callback (or a job callback) raises an exception and you don’t catch it yourself, PTB routes it to
Application.process_error(). (docs.python-telegram-bot.org) process_error()then forwards the exception to all error handlers you registered viaApplication.add_error_handler(...). (docs.python-telegram-bot.org)- The error-handler callback receives the originating
update(orNone) and acontextwhosecontext.erroris the exception. (github-wiki-see.page) - If one error handler raises
ApplicationHandlerStop, PTB stops calling further error handlers and (when the error came from update processing) stops processing that update in any remaining handler groups. (docs.python-telegram-bot.org) - If you register no error handler, PTB will log unhandled exceptions. (github-wiki-see.page)
Minimal pattern (PTB v20+)
from telegram.ext import Application, ContextTypes
async def on_error(update, context: ContextTypes.DEFAULT_TYPE) -> None:
err = context.error # the exception
# log it, notify admins, etc.
app = Application.builder().token("TOKEN").build()
app.add_error_handler(on_error) # receives every uncaught error in the botadd_error_handler(..., block=True) controls whether PTB awaits each error handler before running the next one. (docs.python-telegram-bot.org)
Practical guidance
- Use
try/exceptinside specific handlers for “expected” failures (API 403, timeouts, parse errors), and reserve the global error handler as a last resort for truly unexpected exceptions. (github-wiki-see.page)
Citations:
- 1: https://docs.python-telegram-bot.org/en/v21.9/telegram.ext.application.html
- 2: https://docs.python-telegram-bot.org/en/v21.9/telegram.ext.application.html
- 3: https://github-wiki-see.page/m/python-telegram-bot/python-telegram-bot/wiki/Exceptions%2C-Warnings-and-Logging
- 4: https://docs.python-telegram-bot.org/en/v21.9/telegram.ext.application.html
- 5: https://github-wiki-see.page/m/python-telegram-bot/python-telegram-bot/wiki/Exceptions%2C-Warnings-and-Logging
- 6: https://docs.python-telegram-bot.org/en/v21.9/telegram.ext.application.html
- 7: https://github-wiki-see.page/m/python-telegram-bot/python-telegram-bot/wiki/Exceptions%2C-Warnings-and-Logging
🏁 Script executed:
# Check the full main.py file around lines 126-139
head -n 160 telegram_bot/main.py | tail -n 50Repository: Reya-Labs/reya-python-sdk
Length of output: 1607
🏁 Script executed:
# Search for error handlers in the codebase
rg "add_error_handler|process_error" .Repository: Reya-Labs/reya-python-sdk
Length of output: 51
🏁 Script executed:
# Check for any try/except blocks around the handler logic
rg "try:|except" telegram_bot/main.py -A 2 -B 2Repository: Reya-Labs/reya-python-sdk
Length of output: 1345
Use ApplicationHandlerStop for blocked users instead of raising Exception.
Authorization rejection is normal control flow, not an error. Raising a generic exception routes every rejected update through PTB's error pipeline and creates noisy stack traces in logs that obscure actual problems.
Suggested PTB-native short-circuit
- from telegram.ext import TypeHandler
+ from telegram.ext import ApplicationHandlerStop, TypeHandler
...
async def _check_user(update: Update, context) -> None:
if not update.effective_user:
return
uid = update.effective_user.id
if uid not in allowed_user_ids:
logger.warning("Rejected update from unauthorised user %d", uid)
if update.message:
await update.message.reply_text("You are not authorised to use this bot.")
- raise Exception("Unauthorised")
+ raise ApplicationHandlerStop🧰 Tools
🪛 Ruff (0.15.4)
[warning] 129-129: Unused function argument: context
(ARG001)
[warning] 137-137: Create your own exception
(TRY002)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@telegram_bot/main.py` around lines 126 - 139, The _check_user handler
currently raises a generic Exception to stop processing unauthorized updates;
change this to raise telegram.ext.ApplicationHandlerStop (import
ApplicationHandlerStop) so PTB treats it as normal short-circuit control flow
rather than an error, and keep the existing reply_text call before raising
ApplicationHandlerStop in _check_user (or simply return after sending the reply
if you prefer to avoid raising); update the import list to include
ApplicationHandlerStop and remove the generic Exception usage in the
TypeHandler(Update, _check_user) flow.
Implements a fully-featured Telegram bot that wraps the Reya Python SDK, allowing users to trade perpetuals and spot markets directly from Telegram.
Bot commands:
Files added:
telegram_bot/init.py — package marker
telegram_bot/main.py — entry point, env loading, access control
telegram_bot/bot.py — Telegram Application + all command handlers
telegram_bot/trading.py — async TradingService wrapping ReyaTradingClient
telegram_bot/formatters.py — Markdown message formatters
Also:
pyproject.toml — added python-telegram-bot>=21.0 dependency and package
.env.example — added TELEGRAM_BOT_TOKEN and ALLOWED_USER_IDS vars
Run with: python -m telegram_bot.main
https://claude.ai/code/session_01YGfEGdKJQXu8fQHati5XWX
Summary by CodeRabbit
Release Notes